package com.googlecode.ecritdroid;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jaxen.SimpleNamespaceContext;
import org.jaxen.dom4j.Dom4jXPath;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;

public class LoSTService extends Service {
	private static URL lostUrl;
	private static Map<String,List<Uri>> mapCache = new HashMap<String,List<Uri>>();

	private static LoSTListener locationUpdater;
	private static Location cachedLocation = null;
	
	private static final ScheduledExecutorService scheduler =
	       Executors.newScheduledThreadPool(1);

	private static final long UPDATE_INTERVAL = 30000;
	private static final long UPDATE_DISTANCE = 100;
	private static final String ECRIT_PREF_LOST_URI = "ecrit_lost_uri";
	private static final String LOST_CONTENT_TYPE = "application/lost+xml";
	private static final String TAG = "LoSTService";

	private static Set<Runnable> listeners = new HashSet<Runnable>();
	
	public static final Map<String,String> SERVICES = new HashMap() {{
		put("urn:service:sos", "General Emergency");
		put("urn:service:sos.ambulance", "Ambulance service");
		put("urn:service:sos.animal-control", "Animal control");
		put("urn:service:sos.fire", "Fire service");
		put("urn:service:sos.gas", "Gas leaks and gas emergencies");
		put("urn:service:sos.marine", "Maritime search and rescue");
		put("urn:service:sos.mountain", "Mountain rescue");
		put("urn:service:sos.physician", "Physician referral service");
		put("urn:service:sos.poison", "Poison control center");
		put("urn:service:sos.police", "Police");
	}};

	private static final Map<String,String> NS_MAP = new HashMap<String,String>() {{
		put("lost","urn:ietf:params:xml:ns:lost1");
	}};
	
	public void onCreate() {
		Log.e(TAG, "Started LoST service");
		
		// Subscribe to location updates
		LocationManager locMan = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		locationUpdater = new LoSTListener();
		locMan.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, UPDATE_DISTANCE, locationUpdater);
		locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, UPDATE_DISTANCE, locationUpdater);
		
		// Schedule periodic LoST updates
		scheduler.scheduleWithFixedDelay(new Runnable() {
			public void run() {
				if (cachedLocation == null) return;
				else refresh(cachedLocation.getLatitude(), cachedLocation.getLongitude());
			}
		}, 0, UPDATE_INTERVAL, TimeUnit.MILLISECONDS);
	}

	public static void addListener(Runnable r) {
		listeners.add(r);
	}
	
	public static void removeListener(Runnable r) {
		listeners.remove(r);
	}
	
	private static void notifyListeners() {
		for (Runnable r : listeners) {
			r.run();
		}
	}
	

	public void refresh(double lat, double lon) {
		// Pull the LoST server URL from the preferences
		String lostString = "";
		try {
			SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
			lostString = prefs.getString(ECRIT_PREF_LOST_URI, getString(R.string.default_lost_uri));
			lostUrl = new URL(lostString);
		} catch (MalformedURLException e) {
			Log.e(TAG, "Malformed LoST URI: " + lostString, e);
		}


		Map<String,Set<Uri>> mapping;
		Map<String,List<Uri>> newCache = new HashMap<String,List<Uri>>();
		for (String service : SERVICES.keySet()) {
			mapping = getMapping(lostUrl, lat, lon, service);
			if ((mapping == null) || (mapping.size() <= 0)) 
				continue;

			String serviceNumber = mapping.keySet().iterator().next();
			if (!newCache.containsKey(serviceNumber)) {
				newCache.put(serviceNumber, new ArrayList<Uri>());
			}

			for (Uri uri : mapping.get(serviceNumber)) {
				if (!newCache.get(serviceNumber).contains(uri)) {
					newCache.get(serviceNumber).add(uri);
					Log.e(TAG, "Stored URI " + uri.toString() + " mappings for number " + serviceNumber);
				}
			}
		}
		
		synchronized (mapCache) {
			mapCache = newCache;
		}

		// Notify everybody
		ECRITNotificationUtil.updateNotification(getApplicationContext());
		notifyListeners();
		
		// Dump the cache to the log
		for (String number : mapCache.keySet()) {
			Log.e(TAG, "Stored " + mapCache.get(number).size() + " mappings for number " + number);
		}
	}

	private static Map<String,Set<Uri>> getMapping(URL lostUrl, double lat, double lon, String service) {
		// Encode request
		String req = "";
		req += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
		req += "<findService\n";
		req += "  xmlns=\"urn:ietf:params:xml:ns:lost1\"\n";
		req += "  xmlns:p2=\"http://www.opengis.net/gml\">\n";
		req += "  <location id=\"6020688f1ce1896d\" profile=\"geodetic-2d\">\n";
		req += "    <p2:Point id=\"point1\" srsName=\"urn:ogc:def:crs:EPSG::4326\">\n";
		req += "       <p2:pos>"+ lat +" "+ lon +"</p2:pos>\n";
		req += "    </p2:Point>\n";
		req += "  </location>\n";
		req += "  <service>"+ service +"</service>\n";
		req += "</findService>\n";

		HttpResponse response = null;
		try {
			HttpClient client = new DefaultHttpClient();  
			HttpPost post = new HttpPost(lostUrl.toString());
			post.setHeader("Content-Type", LOST_CONTENT_TYPE);
			post.setEntity(new StringEntity(req));
			response = client.execute(post);
		} catch (Exception e) {
			Log.e("LoST", "Caught exception in LoST HTTP transaction:", e);
			return null;
		}
		if (response == null) return null;

		String serviceNumber = null;
		Set<Uri> uris = new HashSet<Uri>();
		try{
			// Parse HTTP response
			SAXReader reader = new SAXReader();
			Document doc = reader.read(response.getEntity().getContent());

			Dom4jXPath xpath = new Dom4jXPath( "/lost:findServiceResponse/lost:mapping/lost:uri");
			xpath.setNamespaceContext( new SimpleNamespaceContext(NS_MAP));
			List<Element> uriNodes = xpath.selectNodes(doc);
			for (Element node : uriNodes) {
				uris.add(Uri.parse(node.getText()));
			}

			xpath = new Dom4jXPath( "/lost:findServiceResponse/lost:mapping/lost:serviceNumber");
			xpath.setNamespaceContext( new SimpleNamespaceContext(NS_MAP));
			List<Element> snNodes = xpath.selectNodes(doc);
			if (snNodes.size() > 0) serviceNumber = snNodes.get(0).getText();
		} catch (Exception e) {
			Log.e("LoST", "Caught exception parsing LoST response:", e);
			return null;
		}

		if ((serviceNumber == null) || (uris == null) || (uris.size() == 0)) {
			return null;
		}

		Map<String,Set<Uri>> mapping = new HashMap<String,Set<Uri>>();
		mapping.put(serviceNumber, uris);
		return mapping;
	}

	public static boolean validServiceNumber(String number) {
		if (mapCache == null) return false;
		return mapCache.keySet().contains(number);
	}

	public static Set<String> getServiceNumbers() {
		return mapCache.keySet();
	}

	public static List<Uri> getURIs(String number) {
		if (mapCache == null) return null;
		else return mapCache.get(number);
	}

	public static boolean hasMappings() {
		return mapCache.size() > 0;
	}

	public static class LoSTListener implements LocationListener {
		public void onLocationChanged(Location loc) {
			cachedLocation = loc;
			// TODO: Check containment within service boundary
		}

		// Don't care about anything else
		public void onProviderDisabled(String arg0) {}
		public void onProviderEnabled(String arg0) {}
		public void onStatusChanged(String arg0, int arg1, Bundle arg2) {}

	}
	
	@Override
	public IBinder onBind(Intent arg0) {
		return null;
	}

}
